import {type Falsy, noop} from './functional.js'

export function closeIterator(iterator: Iterator<unknown>, value?: unknown) {
    iterator.return?.(value)
}

export function pipe<A>(a: A): A
export function pipe<A, B>(a: A, ab: (a: A) => B): B
export function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
export function pipe<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D
export function pipe<A, B, C, D, E>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E): E
export function pipe<A, B, C, D, E, F>(
    a: A,
    ab: (a: A) => B,
    bc: (b: B) => C,
    cd: (c: C) => D,
    de: (d: D) => E,
    ef: (d: E) => F,
): F
export function pipe<A, B, C, D, E, F, G>(
    a: A,
    ab: (a: A) => B,
    bc: (b: B) => C,
    cd: (c: C) => D,
    de: (d: D) => E,
    ef: (d: E) => F,
    fg: (d: F) => G,
): G
export function pipe(a: unknown, ...transformers: ((x: unknown) => unknown)[]): unknown {
    for (const transformer of transformers)
        a = transformer(a)

    return a
}

export function* range() {
    let i = -1

    for (;;)
        yield ++i
}

export function drop(count: number) {
    return function* <T>(iterator: IterableIterator<T>) {
        while (count-- > 0)
            if (iterator.next().done)
                break

        yield* iterator
    }
}

export function take(limit: number) {
    return function* <T>(iterable: Iterable<T>) {
        if (limit > 0)
            for (const value of iterable) {
                yield value

                if (--limit <= 0)
                    break
            }
    }
}

type Transformer<In, Out> = (iterable: Iterable<In>) => Iterable<Out>

export function filter<T>(predicate: BooleanConstructor): Transformer<T, Exclude<T, Falsy>>
export function filter<T, G extends T>(predicate: (value: T) => value is G): Transformer<T, G>
export function filter<T>(predicate: (value: T) => boolean): Transformer<T, T>
export function filter<T>(predicate: (value: T) => boolean): Transformer<T, T> {
    return function* (iterable: Iterable<T>) {
        for (const value of iterable)
            if (predicate(value))
                yield value
    }
}

export function map<T, R>(mapper: (value: T) => R) {
    return function* (iterable: Iterable<T>) {
        for (const value of iterable)
            yield mapper(value)
    }
}

function isIterable(value: unknown): value is Iterable<unknown> {
    return value !== null && typeof value == 'object' && Symbol.iterator in value
}

export function* flat<T>(iterable: Iterable<T | Iterable<T>>) {
    for (const value of iterable)
        if (isIterable(value))
            yield* value
        else
            yield value
}

// TODO flatDeep
// type T_OR_Iterable_T<T> = T extends Iterable<infer G> ? T_OR_Iterable_T<G> : T

// export function* flatDeep(iterable) {
//     for (const value of iterable)
//         if (value != undefined && value[Symbol.iterator])
//             yield* flatDeep(value);
//         else
//             yield value;
// }

export function flatMap<T, R>(mapper: (value: T) => R | Iterable<R>) {
    return function* (iterable: Iterable<T>) {
        yield* flat(map(mapper)(iterable))
    }
}

export function* flatStrings<T extends string>(iterable: Iterable<T | Iterable<T>>): Iterable<T> {
    for (const value of iterable)
        if (typeof value == 'string')
            yield value
        else
            yield* value
}

// export function* flatStringsDeep(iterable) {
//     for (const value of iterable)
//         if (value != undefined && typeof value != 'string' && Symbol.iterator in Object(value))
//             yield* flatStringsDeep(value);
//         else
//             yield value;
// }

export function tap<T>(tapper: (value: T) => unknown) {
    return function* (iterable: Iterable<T>) {
        for (const value of iterable) {
            tapper(value)
            yield value
        }
    }
}

export type IterableTuple<T> = {
    [K in keyof T]: Iterable<T[K]>
}

export function concatWith<A extends readonly unknown[]>(...iterables: [...IterableTuple<A>]) {
    return function* <T>(iterable: Iterable<T>) {
        yield* iterable
        for (const iterable of iterables)
            yield* iterable
    }
}

export function* zip<A extends readonly unknown[]>(...iterables: [...IterableTuple<A>]): Iterable<A> {
    const iterators = iterables.map(iterable => iterable[Symbol.iterator]())

    try {
        for (;;) {
            const iteratorResults = iterators.map(iterator => iterator.next())

            if (iteratorResults.some(({done}) => done))
                break

            yield iteratorResults.map(({value}: {value: A[number]}) => value) as unknown as A
        }
    } finally {
        for (const iterator of iterators)
            closeIterator(iterator)
    }
}

// export function* zipLongest<T>(filler: T, ...iterables: Iterable<T>[]) {
//     const iterators = iterables.map(iterable => iterable[Symbol.iterator]());

//     for (;;) {
//         const iteratorResults = iterators.map(iterator => iterator.next());

//         if (iteratorResults.every(({done}) => done))
//             break;

//         yield iteratorResults.map(({value, done}) => done ? filler : value as T);
//     }
// }

export function* reverse<T>(iterable: T[] | Iterable<T>) {
    const array = Array.isArray(iterable)
        ? iterable
        : [...iterable]

    for (let i = array.length - 1; i >= 0; i--)
        yield array[i]
}

export type Reducer<V, A = V> = (accumulator: A, value: V) => A

export function accumulate<In, Out = In>(reducer: Reducer<In, In | Out>): Transformer<In, In | Out>
export function accumulate<In, Out = In>(reducer: Reducer<In, Out>, seed: Out): Transformer<In, Out>
export function accumulate<In, Out = In>(reducer: Reducer<In | Out, In | Out>, seed?: Out): Transformer<In, In | Out> {
    return function* (iterable: Iterable<In>) {
        let accumulator: In | Out
        const iterator = makeContinuable(iterable)

        if (seed === undefined) {
            const firstResult = iterator.next()

            if (firstResult.done)
                return

            yield accumulator = firstResult.value
        } else
            accumulator = seed

        for (const value of iterator)
            yield accumulator = reducer(accumulator, value)
    }
}

export function reduce<In>(reducer: Reducer<In, In>): (iterable: Iterable<In>) => In
export function reduce<In, Out>(reducer: Reducer<In, In | Out>): (iterable: Iterable<In>) => In | Out
export function reduce<In, Out>(reducer: Reducer<In, Out>, seed: Out): (iterable: Iterable<In>) => Out
export function reduce<In, Out>(reducer: Reducer<In | Out, In | Out>, seed?: Out) {
    return (iterable: Iterable<In>) => {
        let accumulator: In | Out
        const iterator = makeContinuable(iterable)

        if (seed === undefined) {
            const result = iterator.next()

            if (result.done)
                throw Error('Initial value is necessary to reduce empty iterator')

            accumulator = result.value
        } else
            accumulator = seed

        for (const value of iterator)
            accumulator = reducer(accumulator, value)

        return accumulator
    }
}

export function some<T>(predicate: (value: T) => boolean) {
    return function(iterator: Iterable<T>) {
        for (const value of iterator)
            if (predicate(value))
                return true

        return false
    }
}

export function includes<T>(needle: T) {
    return some<T>(value => value === needle)
}

export function every<T>(predicate: (value: T) => boolean) {
    return function(iterator: Iterable<T>) {
        for (const value of iterator)
            if (!predicate(value))
                return false

        return true
    }
}

export function forEach<T>(func: (value: T) => void = noop) {
    return (iterable: Iterable<T>) => {
        for (const value of iterable)
            func(value)
    }
}

// жаль, нельзя использовать Array.from вместо toArray из-за плохой автовыводимости типов
export const toArray: <T>(iterable: Iterable<T> | ArrayLike<T>) => T[] = Array.from

export function makeContinuable<T>(iterable: Iterable<T>): IterableIterator<T> {
    const iterator: Iterator<T> = iterable[Symbol.iterator]()

    return {
        [Symbol.iterator](this: IterableIterator<T>) {
            return this
        },
        next() {
            return iterator.next()
        },
    }
}
